/** */
package io.libp2p.discovery.mdns.impl;

import io.libp2p.discovery.mdns.impl.constants.DNSConstants;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

/**
 * DNSMessage define a DNS message either incoming or outgoing.
 *
 * @author Werner Randelshofer, Rick Blair, Pierre Frisch
 */
public abstract class DNSMessage {

  /** */
  public static final boolean MULTICAST = true;

  /** */
  public static final boolean UNICAST = false;

  // protected DatagramPacket _packet;
  // protected int _off;
  // protected int _len;
  // protected byte[] _data;

  private int _id;

  boolean _multicast;

  private int _flags;

  protected final List<DNSQuestion> _questions;

  protected final List<DNSRecord> _answers;

  protected final List<DNSRecord> _authoritativeAnswers;

  protected final List<DNSRecord> _additionals;

  /**
   * @param flags
   * @param id
   * @param multicast
   */
  protected DNSMessage(int flags, int id, boolean multicast) {
    super();
    _flags = flags;
    _id = id;
    _multicast = multicast;
    _questions = Collections.synchronizedList(new LinkedList<DNSQuestion>());
    _answers = Collections.synchronizedList(new LinkedList<DNSRecord>());
    _authoritativeAnswers = Collections.synchronizedList(new LinkedList<DNSRecord>());
    _additionals = Collections.synchronizedList(new LinkedList<DNSRecord>());
  }

  // public DatagramPacket getPacket() {
  // return _packet;
  // }
  //
  // public int getOffset() {
  // return _off;
  // }
  //
  // public int getLength() {
  // return _len;
  // }
  //
  // public byte[] getData() {
  // if ( _data == null ) _data = new byte[DNSConstants.MAX_MSG_TYPICAL];
  // return _data;
  // }

  /**
   * @return message id
   */
  public int getId() {
    return (_multicast ? 0 : _id);
  }

  /**
   * @param id the id to set
   */
  public void setId(int id) {
    this._id = id;
  }

  /**
   * @return message flags
   */
  public int getFlags() {
    return _flags;
  }

  /**
   * @param flags the flags to set
   */
  public void setFlags(int flags) {
    this._flags = flags;
  }

  /**
   * @return true if multicast
   */
  public boolean isMulticast() {
    return _multicast;
  }

  /**
   * @return list of questions
   */
  public Collection<? extends DNSQuestion> getQuestions() {
    return _questions;
  }

  /**
   * @return number of questions in the message
   */
  public int getNumberOfQuestions() {
    return this.getQuestions().size();
  }

  public List<DNSRecord> getAllAnswers() {
    List<DNSRecord> aList =
        new ArrayList<DNSRecord>(
            _answers.size() + _authoritativeAnswers.size() + _additionals.size());
    aList.addAll(_answers);
    aList.addAll(_authoritativeAnswers);
    aList.addAll(_additionals);
    return aList;
  }

  /**
   * @return list of answers
   */
  public Collection<? extends DNSRecord> getAnswers() {
    return _answers;
  }

  /**
   * @return number of answers in the message
   */
  public int getNumberOfAnswers() {
    return this.getAnswers().size();
  }

  /**
   * @return list of authorities
   */
  public Collection<? extends DNSRecord> getAuthorities() {
    return _authoritativeAnswers;
  }

  /**
   * @return number of authorities in the message
   */
  public int getNumberOfAuthorities() {
    return this.getAuthorities().size();
  }

  /**
   * @return list of additional answers
   */
  public Collection<? extends DNSRecord> getAdditionals() {
    return _additionals;
  }

  /**
   * @return number of additional in the message
   */
  public int getNumberOfAdditionals() {
    return this.getAdditionals().size();
  }

  /**
   * Check is the response code is valid<br>
   * The only valid value is zero all other values signify an error and the message must be ignored.
   *
   * @return true if the message has a valid response code.
   */
  public boolean isValidResponseCode() {
    return (_flags & DNSConstants.FLAGS_RCODE) == 0;
  }

  /**
   * Returns the operation code value. Currently only standard query 0 is valid.
   *
   * @return The operation code value.
   */
  public int getOperationCode() {
    return (_flags & DNSConstants.FLAGS_OPCODE) >> 11;
  }

  /**
   * Check if the message is truncated.
   *
   * @return true if the message was truncated
   */
  public boolean isTruncated() {
    return (_flags & DNSConstants.FLAGS_TC) != 0;
  }

  /**
   * Check if the message is an authoritative answer.
   *
   * @return true if the message is an authoritative answer
   */
  public boolean isAuthoritativeAnswer() {
    return (_flags & DNSConstants.FLAGS_AA) != 0;
  }

  /**
   * Check if the message is a query.
   *
   * @return true is the message is a query
   */
  public boolean isQuery() {
    return (_flags & DNSConstants.FLAGS_QR_MASK) == DNSConstants.FLAGS_QR_QUERY;
  }

  /**
   * Check if the message is a response.
   *
   * @return true is the message is a response
   */
  public boolean isResponse() {
    return (_flags & DNSConstants.FLAGS_QR_MASK) == DNSConstants.FLAGS_QR_RESPONSE;
  }

  /**
   * Check if the message is empty
   *
   * @return true is the message is empty
   */
  public boolean isEmpty() {
    return (this.getNumberOfQuestions()
            + this.getNumberOfAnswers()
            + this.getNumberOfAuthorities()
            + this.getNumberOfAdditionals())
        == 0;
  }

  /** Debugging. */
  String print() {
    final StringBuilder sb = new StringBuilder(200);
    sb.append(this.toString());
    sb.append("\n");
    for (final DNSQuestion question : _questions) {
      sb.append("\tquestion:      ");
      sb.append(question);
      sb.append("\n");
    }
    for (final DNSRecord answer : _answers) {
      sb.append("\tanswer:        ");
      sb.append(answer);
      sb.append("\n");
    }
    for (final DNSRecord answer : _authoritativeAnswers) {
      sb.append("\tauthoritative: ");
      sb.append(answer);
      sb.append("\n");
    }
    for (DNSRecord answer : _additionals) {
      sb.append("\tadditional:    ");
      sb.append(answer);
      sb.append("\n");
    }
    return sb.toString();
  }

  /**
   * Debugging.
   *
   * @param data
   * @return data dump
   */
  protected String print(byte[] data) {
    final StringBuilder sb = new StringBuilder(4000);
    for (int off = 0, len = data.length; off < len; off += 32) {
      int n = Math.min(32, len - off);
      if (off < 0x10) {
        sb.append(' ');
      }
      if (off < 0x100) {
        sb.append(' ');
      }
      if (off < 0x1000) {
        sb.append(' ');
      }
      sb.append(Integer.toHexString(off));
      sb.append(':');
      int index = 0;
      for (index = 0; index < n; index++) {
        if ((index % 8) == 0) {
          sb.append(' ');
        }
        sb.append(Integer.toHexString((data[off + index] & 0xF0) >> 4));
        sb.append(Integer.toHexString((data[off + index] & 0x0F) >> 0));
      }
      // for incomplete lines
      if (index < 32) {
        for (int i = index; i < 32; i++) {
          if ((i % 8) == 0) {
            sb.append(' ');
          }
          sb.append("  ");
        }
      }
      sb.append("    ");
      for (index = 0; index < n; index++) {
        if ((index % 8) == 0) {
          sb.append(' ');
        }
        int ch = data[off + index] & 0xFF;
        sb.append(((ch > ' ') && (ch < 127)) ? (char) ch : '.');
      }
      sb.append("\n");

      // limit message size
      if (off + 32 >= 2048) {
        sb.append("....\n");
        break;
      }
    }
    return sb.toString();
  }
}
